#include "JobShopSchedulingEnvironment.h"

#include <algorithm>
#include <iostream>
#include <sstream>
#include <fstream>

#include "../../Util/MyStrUtil.h"
#include "../../Crossover/SinglePoint/SinglePointCrossover.h"

//TODO: this environment could use some refinement in the way that it generates
//the evaluation of a given solution.  What it does right now works fine, but
//I bet there's a better solution.

template <typename T>
JobShopSchedulingEnvironment<T>::JobShopSchedulingEnvironment() {
    throw Unsupported_template_exception();
}

JobShopSchedulingEnvironment<PermutationEncodedSolution>::JobShopSchedulingEnvironment() {
}

template <typename T>
JobShopSchedulingEnvironment<T>::~JobShopSchedulingEnvironment() {
}

Crossover* JobShopSchedulingEnvironment<PermutationEncodedSolution>::configureCrossover() {
    return new SinglePointCrossover<PermutationEncodedSolution>();
}        
 
int JobShopSchedulingEnvironment<PermutationEncodedSolution>::decodeTaskIDToJobNum(int taskID){
    return taskID % jobs.size();
}

int JobShopSchedulingEnvironment<PermutationEncodedSolution>::decodeTaskIDToTaskNum(int taskID){
    return (taskID - decodeTaskIDToJobNum(taskID)) / jobs.size(); 
}

int JobShopSchedulingEnvironment<PermutationEncodedSolution>::encodeJobNumTaskNumToTaskID(int jobNum, int taskNum){
    return (taskNum * jobs.size()) + jobNum;
}

//TODO: note that we're using the copy-constructor to return our reports.
//should we be using some kind of pointer trickery instead?
vector<JSSJobReport> JobShopSchedulingEnvironment<PermutationEncodedSolution>::prepareReport(Solution* solution) {
    PermutationEncodedSolution* pesSolution = (PermutationEncodedSolution*) solution;
    vector<JSSJobReport> jobReports;
    
    vector<long> machineAvailableTimes;
    vector<long> jobAvailableTimes;
    vector<int> jobTaskPos;
    
    //initialize our data
    unsigned int machineCount = machines.size();    
    for (unsigned int i = 0; i < machineCount; ++i) {
        machineAvailableTimes.push_back(0);
    }
    
    unsigned int jobCount = jobs.size();    
    for (unsigned int i = 0; i < jobCount; ++i) {
        JSSJobReport report;
        jobReports.push_back(report);
        jobTaskPos.push_back(0);
        jobAvailableTimes.push_back(0);
    }
    
    //for every entry in our solution...
    unsigned int solutionLength = pesSolution->size();
    for (unsigned int i = 0; i < solutionLength; ++i) {
        //prepare to file a new report
        JSSTaskReport taskReport;
        
        //figure out what job that entry refers to
        int jobNum = decodeTaskIDToJobNum(pesSolution->getElement(i));
        
        //figure out which task-number this job is on
        int taskPos = jobTaskPos[jobNum];
        ++jobTaskPos[jobNum];
        
        //get the task that this job needs done next
        JSSTask task = jobs[jobNum].tasks[taskPos];        
        
        //figure out the time that the machine can start that task
        long machineStartTime = machineAvailableTimes[task.machineRequired];
        
        //figure out the time that the job is available for that task
        long jobStartTime = jobAvailableTimes[jobNum];
        
        //take the later of the two, add the duration of the job, and tell
        //the job and the machine that they are busy until that timestamp arrives
        long startTime = max(machineStartTime, jobStartTime);
        long finishTime = startTime + task.timeRequired;        
        machineAvailableTimes[task.machineRequired] = finishTime;
        jobAvailableTimes[jobNum] = finishTime;
        
        //tell our reporting class the start/stop times for this task
        taskReport.startTime = startTime;
        taskReport.stopTime = finishTime;
        jobReports[jobNum].taskReports.push_back(taskReport);
    }
    
    return jobReports;
}

//TODO: this evaluate function could be made more efficient if we stopped using objects
//to contain the machineAvailableTimes and machineTaskPos information
double JobShopSchedulingEnvironment<PermutationEncodedSolution>::evaluateFitness(Solution* solution){
    vector<JSSJobReport> reports = prepareReport(solution);
    
    //TODO: Is this really the measure of goodness that we want?  We're saying that
    //the goodness is directly related to the ending time of the last job...but what
    //if all of our jobs have a really early finish time, except one?  Is that one 
    //worth more than one where everything gets done at the same time?
    long latestFinishTime = 0;
    
    unsigned int numReports = reports.size();
    
    for (unsigned int i = 0; i < numReports; ++i) {
        latestFinishTime = max(latestFinishTime, reports[i].taskReports.back().stopTime);
    }
    
    return (double) (-1 * latestFinishTime);
}

void JobShopSchedulingEnvironment<PermutationEncodedSolution>::populateSolution(Solution* solution) {
    PermutationEncodedSolution* pesSolution = (PermutationEncodedSolution*) solution;
    
    unsigned int numJobs = jobs.size();
    
    for (unsigned int j = 0; j < numJobs; ++j ) {
        unsigned int numTasks = jobs[j].tasks.size();
        
        for (unsigned int t = 0; t < numTasks; ++t ) {
            pesSolution->addElement(encodeJobNumTaskNumToTaskID((int) j, (int) t));
        }
    }
    
    pesSolution->randomizeContentOrder();
}

void JobShopSchedulingEnvironment<PermutationEncodedSolution>::generateRandomPopulation(vector<Solution*> &population, unsigned int count) {
    for (unsigned int i = 0; i < count; ++i) {
        PermutationEncodedSolution* solution = new PermutationEncodedSolution();
        populateSolution(solution);
        solution->setFitness(evaluateFitness(solution));
        population.push_back(solution);
    }
}

string JobShopSchedulingEnvironment<PermutationEncodedSolution>::formatSolution(Solution* solution) {
    vector<JSSJobReport> reports = prepareReport(solution);
    
    ostringstream returnMe;
    
    unsigned int reportsSize = reports.size();
    for (unsigned int i = 0; i < reportsSize; ++i) {
        unsigned int taskReportsSize = reports[i].taskReports.size();
        
        for (unsigned int j = 0; j < taskReportsSize; ++j) {
            long startTime = reports[i].taskReports[j].startTime;
            long stopTime = reports[i].taskReports[j].stopTime;
            returnMe << "J/T/M:(" << i << "/" << j << "/" << jobs[i].tasks[j].machineRequired << " ): " 
                     << startTime << "-" << stopTime << "  ";
        }
        
        returnMe << endl;
    }
    
    return returnMe.str();
}

void JobShopSchedulingEnvironment<PermutationEncodedSolution>::populateRandomly() {
    //create a set of machines
    for (int i = 0; i < 5; ++i) {
        JSSMachine m;
        ostringstream name;
        name << "Machine #" << i;
        m.machineName = name.str();
        machines.push_back(m);
    }
    
    //create a set of jobs
    for (int j = 0; j <= 6; ++j) {
        JSSJob job;
        
        int desiredTaskCount = MyRandom::randInt(1, 5); 
        //create some tasks for each job
        for (int t = 0; t < desiredTaskCount; ++t) {
            JSSTask task;
            task.machineRequired = MyRandom::randInt(0, machines.size() - 1);
            task.timeRequired = long(MyRandom::randInt(1, 60));
            cout << "J:" << j << " | " << machines[task.machineRequired].machineName << " | " << task.timeRequired << " minutes" << endl;
            job.tasks.push_back(task);
        }
        
        jobs.push_back(job);
    }
}

void JobShopSchedulingEnvironment<PermutationEncodedSolution>::populateFromFile() {
    string filename;
    ifstream fileStream;
    
    cout << "Enter the path/filename of the file you wish to read from:" << endl;
    cin >> filename;
    
    fileStream.open(filename.c_str());
    
    if (!fileStream.good())
        throw Invalid_file_exception();
         
    string line;    
        
    // Read in the machine names from the file into the environment
    while(!fileStream.eof())
    {
        myReadLine(fileStream, line);
        
        if (line.empty()) {
            break;
        }
        
        JSSMachine m;
        
        m.machineName = line;
        
        cout << "Adding Machine: '" << m.machineName << "'" << endl;
        
        machines.push_back(m); 
    }
    
    //preload our jobs collection
    JSSJob job;
    myReadLine(fileStream, job.jobName);
    
    // Read in the jobs from the file into the environment
    while(!fileStream.eof())
    {
        myReadLine(fileStream, line);
        
        //if this isn't an empty line, it represents a task to be added
        //to the current job
        if (!line.empty()) {
            std::istringstream in(line);
            JSSTask t;
            in >> t.machineRequired >> t.timeRequired;
            job.tasks.push_back(t);
            
            cout << job.jobName << ": Adding task: " << t.timeRequired << " time units on " << machines[t.machineRequired].machineName << endl;
        }
        //if this is an empty line, it represents that the next task belongs
        //to a different job.
        else {
            jobs.push_back(job);
            job.tasks.clear();
            myReadLine(fileStream, job.jobName);
        }
        
        cout << line << endl;
    }
    
    fileStream.close();
}

void JobShopSchedulingEnvironment<PermutationEncodedSolution>::configure() {
    cout << "Do you wish to populate this environment" << endl;
    cout << "1) Randomly" << endl;
    cout << "2) From a file" << endl;
    int answer;
    
    while (true) {
        cin >> answer;
        
        if (answer == 1) {
            populateRandomly();
            break;
        }
        else if (answer == 2){
            populateFromFile();
            break;
        }
        
        cout << "Invalid entry.  Try again." << endl;
    }
    
    //TODO: if maxTaskCount * jobs.size() is too big for an int to hold, we're
    //going to break the permutationEncodedSolution.  Should we check for that here?
}

void JobShopSchedulingEnvironment<PermutationEncodedSolution>::configure(string) {
    configure();
}

Environment* JobShopSchedulingEnvironment<PermutationEncodedSolution>::getEnvironment() {
    return new JobShopSchedulingEnvironment<PermutationEncodedSolution>();
}

template <typename T>
string JobShopSchedulingEnvironment<T>::getName()
{
    return "Job Shop Scheduling";
}

template class JobShopSchedulingEnvironment<PermutationEncodedSolution>;
